home
***
CD-ROM
|
disk
|
FTP
|
other
***
search
/
Mac Mania 5
/
MacMania 5.toast
/
/
Internet software
/
NewsWatcher
/
NW Source
/
Source
/
cache.c
< prev
next >
Wrap
Text File
|
1997-01-09
|
23KB
|
812 lines
/*----------------------------------------------------------------------------
cache.c
This module manages the article information cache.
Copyright © 1994-1997, Northwestern University.
----------------------------------------------------------------------------*/
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include "glob.h"
#include "cache.h"
#include "dialog.h"
#include "memutil.h"
#include "text.h"
#include "fileutil.h"
#include "windutil.h"
#include "resutil.h"
#include "ic.h"
#define kCacheResourceType 'ACCH'
#define kCacheGroupArrayID 128
#define kCacheArticleArrayID 129
#define kCacheStringsBlockID 130
typedef struct TGroupInfo {
long offset; /* offset in strings block of group name */
long numCached; /* number of cached articles in this group */
} TGroupInfo;
typedef struct TArticleInfo {
long groupIndex; /* index in group info array, or -1 if entry not used */
long number; /* article number */
long subjectOffset; /* offset in strings block of subject string */
long authorOffset; /* offset in strings block of author string */
unsigned long creationDateTime; /* date/time cache entry was created */
} TArticleInfo;
static Boolean gCacheDirty = false; /* true if cache changed since read from prefs file */
static TGroupInfo **gGroupInfo = nil; /* handle to array of group info */
static long gNumGroupInfo = 0; /* number of elements in group info array */
static TArticleInfo **gArticleInfo = nil; /* handle to cached article info */
static long gNumArticleInfo = 0; /* number of elements in article info array */
static Handle gStrings = nil; /* handle to strings block */
static long gStringsAllocated = 0; /* number of bytes allocated in strings block */
static long gStringsUsed = 0; /* number of bytes used in strings block */
/*----------------------------------------------------------------------------
ValidCache
Validate the cache.
Exit: function result = true if no error.
----------------------------------------------------------------------------*/
static Boolean ValidCache (void)
{
long i, j, numCached;
TArticleInfo *p;
TGroupInfo *q;
unsigned long nowDateTimePlus24Hours;
GetDateTime(&nowDateTimePlus24Hours);
nowDateTimePlus24Hours += 24L*60L*60L;
if (MyGetHandleSize(gGroupInfo) != gNumGroupInfo*sizeof(TGroupInfo)) goto exit;
for (i = 0, q = *gGroupInfo; i < gNumGroupInfo; i++, q++) {
if (q->offset < 0) goto exit;
if (q->offset >= gStringsUsed) goto exit;
if (strlen(*gStrings + q->offset) > 255) goto exit;
numCached = 0;
for (j = 0, p = *gArticleInfo; j < gNumArticleInfo; j++, p++)
if (p->groupIndex == i) numCached++;
if (numCached != q->numCached) goto exit;
}
if (MyGetHandleSize(gArticleInfo) != gNumArticleInfo*sizeof(TArticleInfo)) goto exit;
for (i = 0, p = *gArticleInfo; i < gNumArticleInfo; i++, p++) {
if (p->groupIndex < 0) goto exit;
if (p->groupIndex >= gNumGroupInfo) goto exit;
if (p->number <= 0) goto exit;
if (p->subjectOffset < 0) goto exit;
if (p->subjectOffset >= gStringsUsed) goto exit;
if (strlen(*gStrings + p->subjectOffset) > 255) goto exit;
if (p->authorOffset < 0) goto exit;
if (p->authorOffset >= gStringsUsed) goto exit;
if (strlen(*gStrings + p->authorOffset) > 255) goto exit;
if (p->creationDateTime > nowDateTimePlus24Hours) goto exit;
}
return true;
exit:
ErrorMessageNumber(kStrDamagedCache);
return false;
}
/*----------------------------------------------------------------------------
ReadArticleCache
Read the article cache from the prefs file.
Exit: function result = error code.
This function must be called during initialization, when the prefs
file is read.
----------------------------------------------------------------------------*/
OSErr ReadArticleCache (void)
{
OSErr err = noErr;
err = MyGet1Resource(kCacheResourceType, kCacheGroupArrayID, &gGroupInfo);
if (err == resNotFound) {
err = MyNewHandle(0, &gStrings);
if (err != noErr) goto exit;
err = MyNewHandle(0, &gGroupInfo);
if (err != noErr) goto exit;
err = MyNewHandle(0, &gArticleInfo);
if (err != noErr) goto exit;
} else if (err == noErr) {
DetachResource((Handle)gGroupInfo);
gNumGroupInfo = MyGetHandleSize(gGroupInfo) / sizeof(TGroupInfo);
err = MyGet1Resource(kCacheResourceType, kCacheArticleArrayID, &gArticleInfo);
if (err != noErr) goto exit;
DetachResource((Handle)gArticleInfo);
gNumArticleInfo = MyGetHandleSize(gArticleInfo) / sizeof(TArticleInfo);
err = MyGet1Resource(kCacheResourceType, kCacheStringsBlockID, &gStrings);
if (err != noErr) goto exit;
DetachResource(gStrings);
gStringsAllocated = gStringsUsed = MyGetHandleSize(gStrings);
if (!ValidCache()) {
FlushArticleCache();
return userCanceledErr;
}
} else {
goto exit;
}
return noErr;
exit:
MyDisposeHandle(gGroupInfo);
MyDisposeHandle(gArticleInfo);
MyDisposeHandle(gStrings);
gGroupInfo = nil;
gArticleInfo = nil;
gStrings = nil;
gNumGroupInfo = gNumArticleInfo = gStringsAllocated = gStringsUsed = 0;
gCacheDirty = true;
return err;
}
/*----------------------------------------------------------------------------
CompactArticleCache
Compact the cache.
----------------------------------------------------------------------------*/
void CompactArticleCache (void)
{
TGroupInfo **groupInfo = nil, *q1, *q2;
TArticleInfo **articleInfo = nil, *p1, *p2;
Handle strings = nil;
long numGroupInfo, numArticleInfo, stringsUsed, size, i, j, k;
OSErr err = noErr;
short sLen, aLen;
unsigned long nowDateTimeMinus60Days;
if (gGroupInfo == nil) return;
GetDateTime(&nowDateTimeMinus60Days);
nowDateTimeMinus60Days -= 60L*24L*60L*60L;
MySetHandleSize(gStrings, gStringsUsed);
size = MyGetHandleSize(gGroupInfo);
err = MyNewHandle(size, &groupInfo);
if (err != noErr) goto exit;
numGroupInfo = 0;
size = MyGetHandleSize(gArticleInfo);
err = MyNewHandle(size, &articleInfo);
if (err != noErr) goto exit;
BlockMoveData(*gArticleInfo, *articleInfo, size);
numArticleInfo = 0;
size = MyGetHandleSize(gStrings);
err = MyNewHandle(size, &strings);
if (err != noErr) goto exit;
stringsUsed = 0;
for (i = 0, j = 0, q1 = *groupInfo, q2 = *gGroupInfo; j < gNumGroupInfo; j++, q2++) {
if (q2->numCached > 0) {
q1->offset = stringsUsed;
q1->numCached = 0;
strcpy(*strings + stringsUsed, *gStrings + q2->offset);
stringsUsed += strlen(*strings + stringsUsed) + 1;
numGroupInfo++;
q1++;
if (i != j) {
for (k = 0, p1 = *articleInfo; k < gNumArticleInfo; k++, p1++)
if (p1->groupIndex == j) p1->groupIndex = i;
}
i++;
}
}
MySetHandleSize(groupInfo, numGroupInfo * sizeof(TGroupInfo));
for (j = 0, p1 = *articleInfo, p2 = *articleInfo; j < gNumArticleInfo; j++, p2++) {
if (p2->groupIndex >= 0 && p2->groupIndex < numGroupInfo &&
p2->creationDateTime >= nowDateTimeMinus60Days)
{
sLen = strlen(*gStrings + p2->subjectOffset);
aLen = strlen(*gStrings + p2->authorOffset);
p1->groupIndex = p2->groupIndex;
p1->number = p2->number;
strcpy(*strings + stringsUsed, *gStrings + p2->subjectOffset);
p1->subjectOffset = stringsUsed;
stringsUsed += sLen + 1;
strcpy(*strings + stringsUsed, *gStrings + p2->authorOffset);
p1->authorOffset = stringsUsed;
stringsUsed += aLen + 1;
p1->creationDateTime = p2->creationDateTime;
(*groupInfo)[p1->groupIndex].numCached++;
numArticleInfo++;
p1++;
}
}
MySetHandleSize(articleInfo, numArticleInfo * sizeof(TArticleInfo));
MySetHandleSize(strings, stringsUsed);
MyDisposeHandle(gGroupInfo);
gGroupInfo = groupInfo;
gNumGroupInfo = numGroupInfo;
MyDisposeHandle(gArticleInfo);
gArticleInfo = articleInfo;
gNumArticleInfo = numArticleInfo;
MyDisposeHandle(gStrings);
gStrings = strings;
gStringsUsed = gStringsAllocated = stringsUsed;
gCacheDirty = true;
return;
exit:
MyDisposeHandle(strings);
MyDisposeHandle(groupInfo);
MyDisposeHandle(articleInfo);
}
/*----------------------------------------------------------------------------
FlushArticleCache
Flush the cache.
----------------------------------------------------------------------------*/
void FlushArticleCache (void)
{
if (gGroupInfo == nil) return;
MySetHandleSize(gGroupInfo, 0);
gNumGroupInfo = 0;
MySetHandleSize(gArticleInfo, 0);
gNumArticleInfo = 0;
MySetHandleSize(gStrings, 0);
gStringsUsed = gStringsAllocated = 0;
gCacheDirty = true;
}
/*----------------------------------------------------------------------------
WriteArticleCache
Write the article cache to the prefs file.
Entry: newsServerAtStartup = news server name at startup.
Exit: function result = error code.
This function must be called during termination, when the prefs file
is written.
----------------------------------------------------------------------------*/
OSErr WriteArticleCache (Str255 newsServerAtStartup)
{
OSErr err = noErr;
MyICReadSharedPrefs(kICNNTPHost);
if (!gCacheDirty) return noErr;
if (gGroupInfo == nil) {
err = MyNewHandle(0, &gGroupInfo);
if (err != noErr) return err;
err = MyNewHandle(0, &gArticleInfo);
if (err != noErr) return err;
err = MyNewHandle(0, &gStrings);
if (err != noErr) return err;
} else if (!EqualString(gPrefs.newsServerName, newsServerAtStartup, false, true)) {
FlushArticleCache();
} else {
CompactArticleCache();
}
/* Rewrite the three cache resources on the prefs file. */
err = MyReplaceResource(gGroupInfo, kCacheResourceType, kCacheGroupArrayID, "\p");
if (err != noErr) return err;
err = MyReplaceResource(gArticleInfo, kCacheResourceType, kCacheArticleArrayID, "\p");
if (err != noErr) return err;
err = MyReplaceResource(gStrings, kCacheResourceType, kCacheStringsBlockID, "\p");
if (err != noErr) return err;
return noErr;
}
/*----------------------------------------------------------------------------
AddCachedArticle
Add an article to the cache.
Entry: groupName = group name.
number = article number.
subject = subject.
author = author.
Exit: function result = error code.
----------------------------------------------------------------------------*/
OSErr AddCachedArticle (char *groupName, long number, char *subject, char *author)
{
long i;
TArticleInfo *p;
TGroupInfo *q;
long index = -1, groupIndex, subjectOffset, authorOffset, offset;
OSErr err = noErr;
short len, sLen, aLen;
if (gGroupInfo == nil) return noErr;
/* Check to see if this article is already in the cache.
Also get index = index in article info array of a free entry, or
-1 if none. */
for (i = 0, p = *gArticleInfo; i < gNumArticleInfo; i++, p++) {
if (p->groupIndex >= 0) {
if (number == p->number &&
strcmp(groupName, *gStrings + (*gGroupInfo)[p->groupIndex].offset) == 0)
return noErr;
} else {
index = i;
}
}
/* If necessary, add the group name to the strings block and to the group info array. */
for (groupIndex = 0, q = *gGroupInfo; groupIndex < gNumGroupInfo; groupIndex++, q++) {
if (strcmp(groupName, *gStrings + q->offset) == 0) break;
}
if (groupIndex >= gNumGroupInfo) {
len = strlen(groupName);
if (gStringsUsed + len + 1 > gStringsAllocated) {
err = MySetHandleSize(gStrings, gStringsAllocated+1000);
if (err != noErr) return err;
gStringsAllocated += 1000;
}
strcpy(*gStrings + gStringsUsed, groupName);
offset = gStringsUsed;
gStringsUsed += len+1;
err = MySetHandleSize(gGroupInfo, (gNumGroupInfo+1) * sizeof(TGroupInfo));
if (err != noErr) return err;
groupIndex = gNumGroupInfo;
q = &(*gGroupInfo)[groupIndex];
q->offset = offset;
q->numCached = 0;
gNumGroupInfo++;
}
/* Add the subject and author strings to the strings block. */
sLen = strlen(subject);
aLen = strlen(author);
if (gStringsUsed + sLen + aLen + 2 > gStringsAllocated) {
err = MySetHandleSize(gStrings, gStringsAllocated+1000);
if (err != noErr) return err;
gStringsAllocated += 1000;
}
strcpy(*gStrings + gStringsUsed, subject);
subjectOffset = gStringsUsed;
gStringsUsed += sLen+1;
len = strlen(author);
strcpy(*gStrings + gStringsUsed, author);
authorOffset = gStringsUsed;
gStringsUsed += aLen+1;
/* Add the new cache entry to the article info array. */
if (index == -1) {
err = MySetHandleSize(gArticleInfo, (gNumArticleInfo+1) * sizeof(TArticleInfo));
if (err != noErr) return err;
index = gNumArticleInfo;
gNumArticleInfo++;
}
p = &(*gArticleInfo)[index];
p->groupIndex = groupIndex;
p->number = number;
p->subjectOffset = subjectOffset;
p->authorOffset = authorOffset;
GetDateTime(&p->creationDateTime);
/* Increment the counter in the group info array. */
(*gGroupInfo)[groupIndex].numCached++;
gCacheDirty = true;
return noErr;
}
/*----------------------------------------------------------------------------
DeleteCachedArticle
Remove an article from the cache.
Entry: groupName = group name.
number = article number.
Exit: function result = error code.
----------------------------------------------------------------------------*/
OSErr DeleteCachedArticle (char *groupName, long number)
{
long groupIndex, i;
TGroupInfo *q;
TArticleInfo *p;
if (gGroupInfo == nil) return noErr;
for (groupIndex = 0, q = *gGroupInfo; groupIndex < gNumGroupInfo; groupIndex++, q++) {
if (strcmp(groupName, *gStrings + q->offset) == 0) break;
}
if (groupIndex >= gNumGroupInfo) return noErr;
for (i = 0, p = *gArticleInfo; i < gNumArticleInfo; i++, p++) {
if (p->groupIndex == groupIndex && p->number == number) {
p->groupIndex = -1;
(*gGroupInfo)[groupIndex].numCached--;
gCacheDirty = true;
return noErr;
}
}
return noErr;
}
/*----------------------------------------------------------------------------
CompareArticleNumbers
Compare an article number to the article number in a TSubject record.
Entry: *number = article number.
x = pointer to TSubject record
Exit: function result =
-1 if article number < article number in TSubject record.
0 if article number = article number in TSubject record.
+1 if article number > article number in TSubject record.
----------------------------------------------------------------------------*/
static int CompareArticleNumbers (long *number, TSubject *x)
{
if (*number < x->number) {
return -1;
} else if (*number == x->number) {
return 0;
} else {
return +1;
}
}
/*----------------------------------------------------------------------------
AppendOneCachedArticle
Append one cached article for a group to the end of a subject array.
Entry: subjectArray = handle to subject array.
oldNumSubjects = number of elements in original subject array.
*numSubjects = current number of elements in subject array.
strings = handle to strings block for subject window.
number = article number.
subjectOffset = offset of subject string in strings block.
authorOffset = offset of author string in strings block.
Exit: function result = error code.
----------------------------------------------------------------------------*/
static OSErr AppendOneCachedArticle (TSubject **subjectArray, short oldNumSubjects,
long *numSubjects, Handle strings, long number, long subjectOffset,
long authorOffset)
{
TSubject *x;
OSErr err = noErr;
long sOffset, aOffset;
short sLen, aLen;
char state;
/* Check to see if this article was already read from the net. */
state = MyHGetState(subjectArray);
MyHLock(subjectArray);
x = bsearch(&number, *subjectArray, oldNumSubjects, sizeof(TSubject),
(int(*)(const void *, const void *))CompareArticleNumbers);
MyHSetState(subjectArray, state);
if (x != nil) return noErr;
/* Add the subject and author strings to the strings block for the
subject window. */
sLen = strlen(*gStrings + subjectOffset);
aLen = strlen(*gStrings + authorOffset);
sOffset = GetHandleSize(strings);
aOffset = sOffset + sLen + 1;
err = MySetHandleSize(strings, aOffset + aLen + 1);
if (err != noErr) return err;
strcpy(*strings + sOffset, *gStrings + subjectOffset);
strcpy(*strings + aOffset, *gStrings + authorOffset);
/* Add a new TSubject element to the end of the subject array. */
err = MySetHandleSize(subjectArray, (*numSubjects+1)*sizeof(TSubject));
if (err != noErr) return err;
x = &(*subjectArray)[*numSubjects];
x->number = number;
x->subjectOffset = sOffset;
x->authorOffset = aOffset;
x->read = true;
x->collapsed = gPrefs.showThreadsCollapsed;
x->inList = true;
(*numSubjects)++;
return noErr;
}
/*----------------------------------------------------------------------------
AppendCachedArticles
Append all the cached articles for a group to the end of the subject
array for a subject window.
Entry: wind = pointer to subject window.
Exit: function result = error code.
----------------------------------------------------------------------------*/
OSErr AppendCachedArticles (WindowPtr wind)
{
TWindow **info;
CStr255 groupName;
TSubject **subjectArray;
long numSubjects, oldNumSubjects;
long groupIndex, i;
TGroupInfo *q;
TArticleInfo *p;
Handle strings;
OSErr err = noErr;
if (gGroupInfo == nil) return noErr;
info = (TWindow**)GetWRefCon(wind);
strcpy(groupName, *gGroupNames + (**info).groupNameOffset);
subjectArray = (**info).subjectArray;
numSubjects = oldNumSubjects = (**info).numSubjects;
strings = (**info).strings;
for (groupIndex = 0, q = *gGroupInfo; groupIndex < gNumGroupInfo; groupIndex++, q++) {
if (strcmp(groupName, *gStrings + q->offset) == 0) break;
}
if (groupIndex >= gNumGroupInfo) return noErr;
for (i = 0; i < gNumArticleInfo; i++) {
p = &(*gArticleInfo)[i];
if (p->groupIndex == groupIndex) {
err = AppendOneCachedArticle(subjectArray, oldNumSubjects,
&numSubjects, strings, p->number, p->subjectOffset,
p->authorOffset);
if (err != noErr) return err;
}
}
(**info).numSubjects = numSubjects;
return noErr;
}
/*----------------------------------------------------------------------------
AgeArticleCache
Age the cached articles for a group.
Entry: groupName = group name.
low = low article number for this group on the server.
Exit: function result = error code.
All cached articles for the group with article numbers less than
the low article number are deleted.
----------------------------------------------------------------------------*/
OSErr AgeArticleCache (char *groupName, long low)
{
long groupIndex, i;
TGroupInfo *q;
TArticleInfo *p;
if (gGroupInfo == nil) return noErr;
for (groupIndex = 0, q = *gGroupInfo; groupIndex < gNumGroupInfo; groupIndex++, q++) {
if (strcmp(groupName, *gStrings + q->offset) == 0) break;
}
if (groupIndex >= gNumGroupInfo) return noErr;
for (i = 0, p = *gArticleInfo; i < gNumArticleInfo; i++, p++) {
if (p->groupIndex == groupIndex && p->number < low) {
p->groupIndex = -1;
(*gGroupInfo)[groupIndex].numCached--;
gCacheDirty = true;
}
}
return noErr;
}
/*----------------------------------------------------------------------------
DumpArticleCacheToFile
Dump the cache to a text file in human readable format (development
version only).
Entry: fSpec = pointer to file spec.
Exit: function result = error code.
----------------------------------------------------------------------------*/
#ifdef kDevelopmentVersion
OSErr DumpArticleCacheToFile (FSSpec *fSpec)
{
OSErr err = noErr;
short refNum = 0;
CStr255 msg;
Str255 dateString, timeString;
long len, numCache = 0;
TArticleInfo *p;
TGroupInfo *q;
long i;
char state1, state2, state3;
state1 = MyHGetState(gArticleInfo);
state2 = MyHGetState(gArticleInfo);
state3 = MyHGetState(gStrings);
if (gGroupInfo == nil) {
ErrorMessage("There is no cache.");
return userCanceledErr;
}
err = FSpOpenDF(fSpec, fsRdWrPerm, &refNum);
if (err != noErr) goto exit;
MyHLock(gArticleInfo);
MyHLock(gGroupInfo);
MyHLock(gStrings);
for (i = 0, p = *gArticleInfo; i < gNumArticleInfo; i++, p++) {
if (p->groupIndex >= 0) numCache++;
}
for (i = 0, q = *gGroupInfo; i < gNumGroupInfo; i++, q++) {
sprintf(msg, "%9ld cached articles from %s\r", q->numCached, *gStrings + q->offset);
len = strlen(msg);
MyFSWriteNoCache(refNum, &len, msg, nil);
}
sprintf(msg, "\r%9ld total articles in cache\r\r%9ld total bytes in cache\r\r",
numCache, MyGetHandleSize(gGroupInfo) + MyGetHandleSize(gArticleInfo) +
MyGetHandleSize(gStrings));
len = strlen(msg);
MyFSWriteNoCache(refNum, &len, msg, nil);
for (i = 0, p = *gArticleInfo; i < gNumArticleInfo; i++, p++) {
if (p->groupIndex >= 0) {
sprintf(msg, "%ld\r %s:%ld\r", i,
*gStrings + (*gGroupInfo)[p->groupIndex].offset,
p->number);
len = strlen(msg);
MyFSWriteNoCache(refNum, &len, msg, nil);
sprintf(msg, " %s\r", *gStrings + p->subjectOffset);
len = strlen(msg);
MyFSWriteNoCache(refNum, &len, msg, nil);
sprintf(msg, " %s\r", *gStrings + p->authorOffset);
len = strlen(msg);
MyFSWriteNoCache(refNum, &len, msg, nil);
IUDateString(p->creationDateTime, shortDate, dateString);
IUTimeString(p->creationDateTime, false, timeString);
p2cstr(dateString);
p2cstr(timeString);
sprintf(msg, " Created %s %s\r", dateString, timeString);
len = strlen(msg);
MyFSWriteNoCache(refNum, &len, msg, nil);
} else {
sprintf(msg, "%ld\r *** unused ***\r", i);
len = strlen(msg);
MyFSWriteNoCache(refNum, &len, msg, nil);
}
}
MyHSetState(gArticleInfo, state1);
MyHSetState(gGroupInfo, state2);
MyHSetState(gStrings, state3);
MyFSClose(refNum, nil);
return noErr;
exit:
if (refNum != 0) MyFSClose(refNum, nil);
MyHSetState(gArticleInfo, state1);
MyHSetState(gGroupInfo, state2);
MyHSetState(gStrings, state3);
return err;
}
#endif
/*----------------------------------------------------------------------------
DisplayArticleCache
Display the cache in a text window in human readable format (development
version only).
Exit: function result = error code.
----------------------------------------------------------------------------*/
#ifdef kDevelopmentVersion
OSErr DisplayArticleCache (void)
{
FSSpec fSpec;
OSErr err = noErr;
Handle text = nil;
short refNum = 0;
long len;
WindowPtr wind;
err = CreateTemporaryFile(&fSpec, kNewsWatcherSignature, 'ttxt', 'TEXT');
if (err != noErr) goto exit;
err = DumpArticleCacheToFile(&fSpec);
if (err != noErr) goto exit;
err = FSpOpenDF(&fSpec, fsRdPerm, &refNum);
if (err != noErr) goto exit;
err = MyNewHandle(kMaxShort, &text);
if (err != noErr) goto exit;
len = kMaxShort;
MyHLock(text);
err = FSRead(refNum, &len, *text);
MyHUnlock(text);
MySetHandleSize(text, len);
MyFSClose(refNum, nil);
err = MakeNewTextWindow("\pArticle Cache", 0, nil, text, &wind);
if (err != noErr) goto exit;
MyDisposeHandle(text);
return noErr;
exit:
if (refNum != 0) MyFSClose(refNum, nil);
MyDisposeHandle(text);
return err;
}
#endif